//
//  FolderModel.m
//

#import "FolderModel.h"

#import "Controller.h"
#import "PreferencesData.h"
#import "ThumbData.h"
#import "Utility.h"

#import <sys/event.h>
#import <unistd.h>


#import "Epeg.h"


static NSLock				*mapLock = nil;
static NSMutableDictionary	*mapPath2FMByName = nil;
static NSMutableDictionary	*mapPath2FMByDate = nil;
static NSMutableDictionary	*mapPath2FMByType = nil;
static NSMutableDictionary	*mapPath2FMByThumb = nil;


// Default the thumbs to something
// TODO: Perhaps this could come from user preferences?
static int maxThumbWidth = 160;
static int maxThumbHeight = 160;

NSString *FMItemsAddedNotification = @"FMItemsAddedNotification";
NSString *FMItemsRemovedNotification = @"FMItemsRemovedNotification";
NSString *FMItemsUpdatedNotification = @"FMItemsUpdatedNotification";
NSString *FMThumbAvailableNotification = @"FMThumbAvailableNotification";
NSString *FMThumbsCompletedNotification = @"FMThumbsCompletedNotification";


#define NO_DATA		0
#define HAS_DATA	1


@implementation FolderModel

- (int) refCount
{
	return refCount;
}

- (int) addRef
{
	++refCount;
	return refCount;
}

- (int) releaseRef
{
	if (refCount > 0)
	{
		--refCount;
	}
	return refCount;
}

//------------------------------------------------------------------------------
- (void) stopThread
{
	*pExitThread = YES;

	// Get the threads to wake up and exit
	[workListLock unlockWithCondition: HAS_DATA];
}

//------------------------------------------------------------------------------

// Either returns an existing FolderModel or creates a new one, initializing it 
// with the contents of the folder
+ (FolderModel*) instance:(NSString *)path  sort:(int)sort  stop:(BOOL*)pStopLoading
{
	if (nil == mapPath2FMByName)
	{
		mapLock = [[NSLock alloc] init];
		mapPath2FMByName = [[NSMutableDictionary alloc] initWithObjectsAndKeys: nil];
		mapPath2FMByDate = [[NSMutableDictionary alloc] initWithObjectsAndKeys: nil];
		mapPath2FMByType = [[NSMutableDictionary alloc] initWithObjectsAndKeys: nil];
		mapPath2FMByThumb = [[NSMutableDictionary alloc] initWithObjectsAndKeys: nil];
	}

	NSString *lcPath = [path lowercaseString];

	FolderModel *fm = nil;
	if (SORT_BY_NAME == sort)
	{
		fm = [mapPath2FMByName objectForKey: lcPath];
	}
	else
	if (SORT_BY_DATE == sort)
	{
		fm = [mapPath2FMByDate objectForKey: lcPath];
	}
	else
	if (SORT_BY_TYPE == sort)
	{
		fm = [mapPath2FMByType objectForKey: lcPath];
	}
	else
	if (SORT_BY_THUMBSTATE == sort)
	{
		fm = [mapPath2FMByThumb objectForKey: lcPath];
	}

	if (nil == fm || [fm refCount] < 1)
	{
		[mapLock lock];
		if (SORT_BY_NAME == sort)
		{
			fm = [mapPath2FMByName objectForKey: lcPath];
		}
		else
		if (SORT_BY_DATE == sort)
		{
			fm = [mapPath2FMByDate objectForKey: lcPath];
		}
		else
		if (SORT_BY_TYPE == sort)
		{
			fm = [mapPath2FMByType objectForKey: lcPath];
		}
		else
		if (SORT_BY_THUMBSTATE == sort)
		{
			fm = [mapPath2FMByThumb objectForKey: lcPath];
		}

		if (nil == fm || [fm refCount] <= 1)
		{
			// Is it in another list?
			FolderModel *fmOther = [mapPath2FMByName objectForKey: lcPath];
			if (nil == fmOther)
			{
				fmOther = [mapPath2FMByDate objectForKey: lcPath];
			}
			if (nil == fmOther)
			{
				fmOther = [mapPath2FMByType objectForKey: lcPath];
			}
			if (nil == fmOther)
			{
				fmOther = [mapPath2FMByThumb objectForKey: lcPath];
			}

			if (nil != fmOther)
			{
				fm = [[FolderModel alloc] initWithFM: fmOther  sort:sort  stop:pStopLoading];
			}
			else
			{
				fm = [[FolderModel alloc] initWithPath: path  sort:sort  stop:pStopLoading];
			}

			if (SORT_BY_NAME == sort)
			{
				[mapPath2FMByName setValue :fm  forKey: lcPath];
			}
			else
			if (SORT_BY_DATE == sort)
			{
				[mapPath2FMByDate setValue :fm  forKey: lcPath];
			}
			else
			if (SORT_BY_TYPE == sort)
			{
				[mapPath2FMByType setValue :fm  forKey: lcPath];
			}
			else
			if (SORT_BY_THUMBSTATE == sort)
			{
				[mapPath2FMByThumb setValue :fm  forKey: lcPath];
			}

			[fm release];
		}
		[mapLock unlock];
	}
	else
	{
		[fm addRef];
	}

	return fm;
}

//------------------------------------------------------------------------------
+ (void) releaseInstance:(FolderModel*)fm
{
	if (nil == fm)
	{
		return;
	}

	int count = [fm releaseRef];

	if (count <= 0)
	{
		NSString *path = [[fm path] lowercaseString];
		[fm stopThread];

		[mapLock lock];

		if (SORT_BY_NAME == [fm sortedBy])
		{
			[mapPath2FMByName removeObjectForKey: path];
		}
		else
		if (SORT_BY_DATE == [fm sortedBy])
		{
			[mapPath2FMByDate removeObjectForKey: path];
		}
		else
		if (SORT_BY_TYPE == [fm sortedBy])
		{
			[mapPath2FMByType removeObjectForKey: path];
		}
		else
		if (SORT_BY_THUMBSTATE == [fm sortedBy])
		{
			[mapPath2FMByThumb removeObjectForKey: path];
		}

		[mapLock unlock];
	}
}

//------------------------------------------------------------------------------
// Instance methods
//------------------------------------------------------------------------------
- (void) commonInit:(NSString*) thePath  sort:(int)sort
{
	refCount = 1;

	path = thePath;
	[path retain];

	sortedBy = sort;

	threadWorkList = [NSMutableArray array];
	[threadWorkList retain];

	activeWorkList = [NSMutableArray array];
	[activeWorkList retain];

	mainThreadWorkList = [NSMutableArray array];
	[mainThreadWorkList retain];
	mainThreadListLock = [[NSLock alloc] init];

	workListLock = [[NSConditionLock alloc] initWithCondition: NO_DATA];
	listLock = [[NSLock alloc] init];
	movieLock = [[NSLock alloc] init]; 

	excludedMovieExtensions = [NSMutableSet set];
	[excludedMovieExtensions retain];

	pExitThread = malloc(sizeof(BOOL));
	*pExitThread = NO;

	int numThreads = 4;
	int idx;
	for (idx = 0; idx < numThreads; ++idx)
	{
		[NSThread 
			detachNewThreadSelector: @selector(thumbDataThread:)
			toTarget: self
			withObject: nil];
	}
}

//------------------------------------------------------------------------------
- (NSMutableArray*) childrenBulk:(NSString*) thePath  stop:(BOOL*)pStopLoading
{
	NSMutableArray *tdl = [NSMutableArray array];
	[tdl retain];


	Controller *controller = [Controller sharedController];

	FSRef container;
	OSErr err = FSPathMakeRef((const UInt8 *) [thePath UTF8String], &container, NULL);
	if (noErr != err)
	{
		NSLog(@"FSPathMakeRef failed with %d (on %@)", err, thePath);
	}
	else
	{
		FSIterator iterator;
		err = FSOpenIterator(&container, kFSIterateFlat, &iterator);
		if (noErr != err)
		{
			NSLog(@"FSOpenIterator failed with %d (on %@)", err, thePath);
		}
		else
		{
			// Use up to four pages of memory
			ItemCount maxObjects = ((4096 * 4) / sizeof(FSCatalogInfo));
			FSCatalogInfo *catalogInfo = malloc(maxObjects * sizeof(FSCatalogInfo));
			FSRef *refs = malloc(maxObjects * sizeof(FSRef));
			NSSize zeroSize = NSMakeSize(0, 0);

			do
			{
				ItemCount numObjects = 0;
				err = FSGetCatalogInfoBulk(
						iterator, 
						maxObjects, 
						&numObjects,
						NULL, 
						kFSCatInfoContentMod | kFSCatInfoDataSizes, 
						catalogInfo,
						refs, 
						NULL, 
						NULL);

				if (noErr == err || errFSNoMoreItems == err)
				{
					int idx;
					for (idx = 0; idx < numObjects; ++idx)
					{
						FSCatalogInfo *itemInfo = &(catalogInfo[idx]);

						UInt8 uintName[PATH_MAX + 1];
						FSRefMakePath (&refs[idx], uintName, PATH_MAX);
						NSString *fullPath = [NSString stringWithUTF8String: (const char *)uintName];
						if (NO == [controller isSupportedFile: fullPath])
						{
							continue;
						}

						CFAbsoluteTime absTime;
						UCConvertUTCDateTimeToCFAbsoluteTime(&itemInfo->contentModDate, &absTime);
						NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate: absTime];
						UInt64 length = (long) (itemInfo->dataLogicalSize);

						ThumbData *td = [[ThumbData alloc] init
							: nil
							fullPath: fullPath
							fileLength: length
							date: date
							imageSize: zeroSize
							thumbState: THUMB_STATE_UNKNOWN];

						[td setMediaType: [Controller mediaTypeForFile: fullPath]];

						[tdl addObject: td];
						[td release];
					}
				}

			} while (noErr == err && (nil == pStopLoading || NO == *pStopLoading));

			if (errFSNoMoreItems == err)
			{
				err = noErr;
			}

			free(refs);
			free(catalogInfo);
			FSCloseIterator(iterator);
		}
	}

	if (noErr != err)
	{
		NSLog(@"FolderModel.childrenBulk failed with %d (on %@)", err, thePath);
		[tdl release];
		return nil;
	}

	return tdl;
}

//------------------------------------------------------------------------------
// A deprecated method, kept around for testing/validation.
- (NSMutableArray*) children:(NSString*) thePath  stop:(BOOL*)pStopLoading
{
	NSFileManager *fileManager = [NSFileManager defaultManager];
	NSMutableArray *children = [NSMutableArray arrayWithArray
		: [fileManager directoryContentsAtPath: thePath]];
	[[Controller sharedController] filterImageFiles: children];

	int count = [children count];
	NSMutableArray *tdl = [NSMutableArray arrayWithCapacity: count];
	[tdl retain];

	// Create a ThumbData with the name & file date for each image
	NSSize zeroSize = NSMakeSize(0, 0);
	int idx;
	for (idx = 0; idx < count && (nil == pStopLoading || NO == *pStopLoading); ++idx)
	{
		NSString *childName = [children objectAtIndex: idx];
		NSString *fullPath = [thePath stringByAppendingPathComponent: childName];

		NSDictionary *attrs = [fileManager fileAttributesAtPath: fullPath traverseLink: NO];
		NSDate *fileDateTime = [attrs objectForKey: NSFileModificationDate];
		NSNumber *fileSize = [attrs objectForKey: NSFileSize];

		ThumbData *td = [[ThumbData alloc] init
			: nil
			fullPath: fullPath
			fileLength: [fileSize longValue]
			date: fileDateTime
			imageSize: zeroSize
			thumbState: THUMB_STATE_UNKNOWN];

		[td setMediaType: [Controller mediaTypeForFile: childName]];

		[tdl addObject: td];
		[td release];
	}

	return tdl;
}

//------------------------------------------------------------------------------
- (id) sortThumbList:(NSMutableArray*) tdl  sort:(int)sort
{
	// Sort appropriately
	if (SORT_BY_NAME == sort)
	{
		[tdl sortUsingFunction: compareThumbDataNames  context: nil];
	}
	else
	if (SORT_BY_DATE == sort)
	{
		[tdl sortUsingFunction: compareThumbDataDates  context: nil];
	}
	else
	if (SORT_BY_TYPE == sort)
	{
		[tdl sortUsingFunction: compareThumbDataTypes  context: nil];
	}
	else
	if (SORT_BY_THUMBSTATE == sort)
	{
		[tdl sortUsingFunction: compareThumbStates  context: nil];
	}

	return nil;
}

//------------------------------------------------------------------------------
- (id) initWithPath:(NSString*) thePath  sort:(int)sort  stop:(BOOL*)pStopLoading
{
	self = [super init];
	if (nil != self)
	{
		[self commonInit: thePath  sort:sort];

		// Load all children of this directory, filter out the directories and non-image files
		tdList = [self childrenBulk:thePath  stop:pStopLoading];
		[self sortThumbList: tdList  sort:sort];

		// Load or generate the thumbnails
		if ([tdList count] > 0)
		{
			[workListLock lock];
			[threadWorkList addObjectsFromArray: tdList];
			[workListLock unlockWithCondition: HAS_DATA];
		}


		// Have a thread auto-detect changes to the folder
//		[NSThread detachNewThreadSelector:@selector(kqueueThread:)  toTarget:self  withObject:nil];
	}

	return self;
}

//------------------------------------------------------------------------------
- (id) initWithFM:(FolderModel*)fm  sort:(int)sort  stop:(BOOL*)pStopLoading
{
	self = [super init];
	if (nil != self)
	{
		[self commonInit: [fm path]  sort:sort];

		// Grab all the items in the given FolderModel, add them to our list.
		int count = [fm count];
		tdList = [NSMutableArray arrayWithCapacity: count];
		[tdList retain];

		int idx;
		for (idx = 0; idx < count && (nil == pStopLoading || NO == *pStopLoading); ++idx)
		{
			[tdList addObject: [fm tdAtIndex: idx]];
		}


		[self sortThumbList: tdList  sort:sort];
	}

	return self;
}

//------------------------------------------------------------------------------
- (void) dealloc
{
	[path release];
	[tdList release];

	free(pExitThread);
	[threadWorkList release];
	[workListLock release];
	[movieLock release];
	[activeWorkList release];
	[mainThreadWorkList release];
	[mainThreadListLock release];
	[excludedMovieExtensions release];
	[listLock release];

	[super dealloc];
}

//------------------------------------------------------------------------------
- (int) sortedBy
{
	return sortedBy;
}

//------------------------------------------------------------------------------
// Returns the directory this FolderModel represents
- (NSString*) path
{
	return path;
}

//------------------------------------------------------------------------------
// Returns the number of image files in this model
- (unsigned) count
{
	return [tdList count];
}

//------------------------------------------------------------------------------
- (unsigned) queueCount
{
	return [threadWorkList count];
}

//------------------------------------------------------------------------------
- (int) indexOfTD:(ThumbData*)td
{
	return [tdList indexOfObject: td];
}

//------------------------------------------------------------------------------
// Returns the ThumbData at 'index'
- (ThumbData*) tdAtIndex:(unsigned) index
{
	return [tdList objectAtIndex: index];
}

//------------------------------------------------------------------------------
- (void) setOperationFile:(NSString*) file
{
	[operationFile release];
	operationFile = file;
	[operationFile retain];
}

//------------------------------------------------------------------------------
- (void) addItems:(NSArray*) thumbArray
{
	if (nil == thumbArray || [thumbArray count] < 1)
	{
		return;
	}

	// Add the items to our internal list
	[listLock lock];

	int idx;
	for (idx = 0; idx < [thumbArray count]; ++idx)
	{
		ThumbData *td = [thumbArray objectAtIndex: idx];
		[tdList addObject: td];
	}
	[listLock unlock];

	// Start working on the thumbnails for the added items
	[workListLock lock];
	[threadWorkList addObjectsFromArray: thumbArray];
	[workListLock unlockWithCondition: HAS_DATA];

	// Sort the list (if this is too expensive, we'll have to add the items one at a time,
	// in the proper slot).
	[listLock lock];
	[self sortThumbList: tdList  sort:sortedBy];
	[listLock unlock];


	NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys
		: thumbArray, FM_NOTIFY_ITEMS, nil, nil];

	// Notify interested parties that items were updated
	[[NSNotificationCenter defaultCenter] postNotificationName 
		: FMItemsAddedNotification
		object: self 
		userInfo: dict];
}

//------------------------------------------------------------------------------
- (void) removeItems:(NSArray*) thumbArray
{
	if (nil == thumbArray || [thumbArray count] < 1)
	{
		return;
	}

	[listLock lock];

	int idx;
	for (idx = 0; idx < [thumbArray count]; ++idx)
	{
		ThumbData *td = [thumbArray objectAtIndex: idx];
		int masterIndex = [tdList indexOfObjectIdenticalTo: td];
		if (masterIndex != NSNotFound)
		{
			[tdList removeObjectAtIndex: masterIndex];
		}
	}
	[listLock unlock];


	NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys
		: thumbArray, FM_NOTIFY_ITEMS, nil, nil];

	// Notify interested parties that items were removed
	[[NSNotificationCenter defaultCenter] postNotificationName 
		: FMItemsRemovedNotification
		object: self 
		userInfo: dict];
}

//------------------------------------------------------------------------------
- (void) updateItems:(NSArray*) thumbArray
{
	if (nil == thumbArray || [thumbArray count] < 1)
	{
		return;
	}

	[listLock lock];
	int idx;
	for (idx = 0; idx < [thumbArray count]; ++idx)
	{
		ThumbData *td = [thumbArray objectAtIndex: idx];
		int masterIndex = [tdList indexOfObjectIdenticalTo: td];
		if (masterIndex != NSNotFound)
		{
			[tdList replaceObjectAtIndex:masterIndex  withObject:td];
		}
	}
	[listLock unlock];


	for (idx = 0; idx < [thumbArray count]; ++idx)
	{
		ThumbData *td = [thumbArray objectAtIndex: idx];
		// Does this need a thumbnail generated?
		if (nil == [td thumbnail])
		{
			[workListLock lock];
			[threadWorkList addObject: td];
			[workListLock unlockWithCondition: HAS_DATA];
		}
	}

	NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys
		: thumbArray, FM_NOTIFY_ITEMS, nil, nil];

	// Notify interested parties that items were updated
	[[NSNotificationCenter defaultCenter] postNotificationName 
		: FMItemsUpdatedNotification
		object: self 
		userInfo: dict];
}

//------------------------------------------------------------------------------
// Called by the thumbnail thread (but executed on the main thread) - we notify
// interested parties.
- (void) thumbAvailable:(ThumbData*) td
{
	NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys
		: td, FM_THUMB_DATA, nil, nil];

	// Notify interested parties that items were updated
	[[NSNotificationCenter defaultCenter] postNotificationName 
		: FMThumbAvailableNotification
		object: self 
		userInfo: dict];
}

//------------------------------------------------------------------------------
- (void) thumbsCompleted:(id) nada
{
	// Notify interested parties that all thumbnails have been loaded/generated
	[[NSNotificationCenter defaultCenter] postNotificationName 
		: FMThumbsCompletedNotification
		object: self 
		userInfo: nil];
}

//------------------------------------------------------------------------------
- (void) processRefresh:(NSMutableArray*) refreshed
{
	// Sort in the same fashion as our internal list
	[self sortThumbList:refreshed  sort:sortedBy];


	// Hunt for differences between the lists (copy the master list so we aren't
	// affected by changes by another thread).
	NSMutableArray *tdMasterList = [NSMutableArray arrayWithArray: tdList];
	int masterIndex = 0;
	int refreshedIndex = 0;
	int masterCount = [tdMasterList count];
	int refreshedCount = [refreshed count];


	NSMutableArray *addedList = [NSMutableArray arrayWithCapacity: 0];
	NSMutableArray *removedList = [NSMutableArray arrayWithCapacity: 0];
	NSMutableArray *updatedList = [NSMutableArray arrayWithCapacity: 0];


	while (masterIndex < masterCount && refreshedIndex < refreshedCount)
	{
		ThumbData *tdMaster = [tdMasterList objectAtIndex: masterIndex];
		ThumbData *tdRefresh = [refreshed objectAtIndex: refreshedIndex];

		int comp;
		if (SORT_BY_NAME == sortedBy)
		{
			comp = compareThumbDataNames(tdMaster, tdRefresh, self);
		}
		else
		if (SORT_BY_DATE == sortedBy)
		{
			comp = compareThumbDataDates(tdMaster, tdRefresh, self);
		}
		else
		if (SORT_BY_TYPE == sortedBy)
		{
			comp = compareThumbDataTypes(tdMaster, tdRefresh, self);
		}
		else
		if (SORT_BY_THUMBSTATE == sortedBy)
		{
			comp = compareThumbStates(tdMaster, tdRefresh, self);
		}

//NSLog(@"comp: %d; master: %@; refresh: %@", comp, [tdMaster name], [tdRefresh name]);

		if (comp < 0)
		{
			// queue thumbdata removal from master
			if (NO == [[tdMaster name] isEqualToString: operationFile])
			{
				[removedList addObject: tdMaster];
			}
			++masterIndex;
		}
		else
		if (0 == comp)
		{
			if (NO == [[tdMaster name] isEqualToString: operationFile])
			{
				// if the file length or file date is different, queue an update notification
				if ([[tdMaster fileDate] compare: [tdRefresh fileDate]] || 
					[tdMaster fileLength] != [tdRefresh fileLength])
				{
					if ([tdMaster fileLength] != [tdRefresh fileLength])
					{
						// The thumbnail more than likely changed, it needs to be reloaded,
						// which will happen automatically (because tdRefresh doesn't have a 
						// thumbnail right now)
					}
					else
					{
						// Only the date changed, re-use, rather than reload, the thumbnail
						[tdRefresh setThumbnail: [tdMaster thumbnail]];
						[tdRefresh setThumbstate: [tdMaster thumbState]];
					}

					[updatedList addObject: tdRefresh];
				}
			}

			++masterIndex;
			++refreshedIndex;
		}
		else
		{
			// queue thumbdata addition to master. The thumbnail also needs to be loaded.
			if (NO == [[tdRefresh name] isEqualToString: operationFile])
			{
				[addedList addObject: tdRefresh];
			}

			++refreshedIndex;
		}
	}

	if (masterCount > refreshedCount)
	{
		while (masterIndex < masterCount)
		{
			ThumbData *td = [tdMasterList objectAtIndex: masterIndex];
			if (NO == [[td name] isEqualToString: operationFile])
			{
				[removedList addObject: td];
			}
			++masterIndex;
		}
	}
	else
	{
		while (refreshedIndex < refreshedCount)
		{
			ThumbData *td = [refreshed objectAtIndex: refreshedIndex];
			if (NO == [[td name] isEqualToString: operationFile])
			{
				[addedList addObject: td];
			}
			++refreshedIndex;
		}
	}


	// Debugging help...
#if 0
NSLog(@" - ignored %@", operationFile);
	int idx;
//NSLog(@"Remove the following items: %d", [removedList count]);
	for (idx = 0; idx < [removedList count]; ++idx)
	{
		ThumbData *td = [removedList objectAtIndex: idx];
NSLog(@" - remove  %@", [td name]);
	}

//NSLog(@"Add the following items: %d", [addedList count]);
	for (idx = 0; idx < [addedList count]; ++idx)
	{
		ThumbData *td = [addedList objectAtIndex: idx];
NSLog(@" - add  %@", [td name]);
	}

//NSLog(@"Update the following items: %d", [updatedList count]);
	for (idx = 0; idx < [updatedList count]; ++idx)
	{
		ThumbData *td = [updatedList objectAtIndex: idx];
NSLog(@" - update %@", [td name]);
	}
#endif

	// Remove any items that need to be removed
	if ([removedList count] > 0)
	{
		[self performSelectorOnMainThread
				:@selector(removeItems:)
				withObject: removedList
				waitUntilDone: NO];
	}

	// Add any items that need to be added
	if ([addedList count] > 0)
	{
		[self performSelectorOnMainThread
				:@selector(addItems:)
				withObject: addedList
				waitUntilDone: NO];
	}

	// And update any items that were updated
	if ([updatedList count] > 0)
	{
		[self performSelectorOnMainThread
				:@selector(updateItems:)
				withObject: updatedList
				waitUntilDone: NO];
	}
}

//------------------------------------------------------------------------------
- (void) refresh
{
	// Load all children of this directory, filter out the directories and non-image files
	NSMutableArray *refreshed = [self childrenBulk: path  stop:nil];
	[self processRefresh: refreshed];


	// If there's a FolderModel sorted by the another key, get it updated, too
	NSString *lcPath = [path lowercaseString];
	FolderModel *fmFirst = nil;
	FolderModel *fmSecond = nil;
	FolderModel *fmThird = nil;

	if (SORT_BY_NAME == sortedBy)
	{
		fmFirst = [mapPath2FMByDate objectForKey: lcPath];
		fmSecond = [mapPath2FMByType objectForKey: lcPath];
		fmThird = [mapPath2FMByThumb objectForKey: lcPath];
	}
	else
	if (SORT_BY_DATE == sortedBy)
	{
		fmFirst = [mapPath2FMByName objectForKey: lcPath];
		fmSecond = [mapPath2FMByType objectForKey: lcPath];
		fmThird = [mapPath2FMByThumb objectForKey: lcPath];
	}
	else
	if (SORT_BY_TYPE == sortedBy)
	{
		fmFirst = [mapPath2FMByName objectForKey: lcPath];
		fmSecond = [mapPath2FMByDate objectForKey: lcPath];
		fmThird = [mapPath2FMByThumb objectForKey: lcPath];
	}
	else
	if (SORT_BY_THUMBSTATE == sortedBy)
	{
		fmFirst = [mapPath2FMByName objectForKey: lcPath];
		fmSecond = [mapPath2FMByDate objectForKey: lcPath];
		fmThird = [mapPath2FMByType objectForKey: lcPath];
	}

	if (nil != fmFirst)
	{
		[fmFirst processRefresh: refreshed];
	}
	if (nil != fmSecond)
	{
		[fmSecond processRefresh: refreshed];
	}
	if (nil != fmThird)
	{
		[fmThird processRefresh: refreshed];
	}

	[refreshed release];
}

//------------------------------------------------------------------------------
- (void) generateThumbOnMainThread:(id) thumbData
{
	if (nil == [thumbData thumbnail])
	{
		NSImage *image = generateThumbnail([thumbData fullPath], maxThumbWidth, maxThumbHeight);
		if (nil != image)
		{
			[thumbData setThumbnail: image];
			[image release];

			[self thumbAvailable: thumbData];
		}
	}
}

//------------------------------------------------------------------------------
- (void) thumbDataThread: (id) nada
{
    EnterMoviesOnThread(0);
	CSSetComponentsThreadMode(kCSAcceptAllComponentsMode);

	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

//NSLog(@"thumbDataThread started for %@", path);

	// Until we're told to end, wait around for items to work on
	while (NO == *pExitThread)
	{
//NSLog(@"thumbDataThread, waiting on lock: %d : %d (rc=%d)", 
//			[threadWorkList count], *pExitThread, [self refCount]);
		ThumbData *td = nil;

		if (NO == [workListLock 
				lockWhenCondition: HAS_DATA  
				beforeDate: [NSDate dateWithTimeIntervalSinceNow: 1]])
		{
			continue;
		}


//		[workListLock lockWhenCondition: HAS_DATA];


		if ([threadWorkList count] > 0)
		{
			td = [threadWorkList objectAtIndex: 0];
			[activeWorkList addObject: td];
			[threadWorkList removeObjectAtIndex: 0];
		}

		[workListLock unlockWithCondition: (0 == [threadWorkList count] ? NO_DATA : HAS_DATA)];

		if (NO == *pExitThread && nil != td)
		{
//NSLog(@"thread; td state: %d; name %@", [td thumbState], [td name]);
			[self processThumbData: td];
//NSLog(@" - after thread; td state: %d; name %@", [td thumbState], [td name]);

			[workListLock lock];
			[activeWorkList removeObjectIdenticalTo: td];
			[workListLock unlock];

			if (0 == [threadWorkList count] && 0 == [activeWorkList count])
			{
				[mainThreadListLock lock];
				while (NO == *pExitThread && 0 != [mainThreadWorkList count])
				{
					td = [mainThreadWorkList objectAtIndex: 0];
					[mainThreadWorkList removeObjectAtIndex: 0];

					[self performSelectorOnMainThread
						: @selector(generateThumbOnMainThread:)
						withObject: td
						waitUntilDone: YES];

					[self saveMovieThumbnail: td];

					usleep(200000);
				}

				if (SORT_BY_THUMBSTATE == sortedBy)
				{
					[self sortThumbList:tdList  sort:sortedBy];
				}

				[mainThreadListLock unlock];

				[self performSelectorOnMainThread
					: @selector(thumbsCompleted:)
					withObject: nil
					waitUntilDone: NO];
			}
		}
	}

//NSLog(@"exit thumbDataThread for %@", path);

	[pool release];
	ExitMoviesOnThread();
}

//------------------------------------------------------------------------------
- (void) processThumbData:(ThumbData*) td
{
	int thumbState = [td thumbState];

	// First pass - grab the EXIF thumbnail, if it's available.
	if (THUMB_STATE_UNKNOWN == thumbState)
	{
//NSLog(@"load EXIF for %@", [td name]);
		int thumbState = THUMB_STATE_NO_EXIF;
		NSImage *thumbnail = nil;
		NSDate *exifDate = nil;

		// Determine the actual image width/height (which may be different than 
		// what's stored in the EXIF info).
		int aWidth = 0, aHeight = 0;
		Epeg_Image *epeg = epeg_file_open([[td fullPath] cStringUsingEncoding:NSUTF8StringEncoding]);
		if (nil != epeg)
		{
			[td setFileType: FILE_TYPE_JPEG];
			epeg_size_get(epeg, &aWidth, &aHeight);
			epeg_close(epeg);
			[td setImageSize: NSMakeSize(aWidth, aHeight)];

			getExifData([td fullPath], nil, &exifDate, &thumbnail);
			if (nil != exifDate)
			{
				[td setExifDate: exifDate];
			}
		}
		else
		{
			[td setFileType: FILE_TYPE_OTHER];

			if (MEDIA_TYPE_MOVIE == [td mediaType])
			{
				thumbnail = [self getMovieThumbnail: td];
			}
		}

		if (nil != thumbnail)
		{
			NSSize sizeThumbnail = [thumbnail size];


			// Check to see if the thumbnail has the same aspect ratio as the image
			// NOTE: At least one manufacturer that supports auto-orientation correction (Kodak)
			// has a thumbnail with landscape dimensions when the photo is portrait. The thumbnail
			// has actually been rotated, but it's filled in with black. I can't find other flags
			// (like an orientation flag) to help determine if the thumbnail is correct...
			BOOL thumbWidthBigger = sizeThumbnail.width >= sizeThumbnail.height;
			NSSize sizeImage = [td imageSize];
			BOOL imageWidthBigger = sizeImage.width >= sizeImage.height;
			if (thumbWidthBigger != imageWidthBigger)
			{
				thumbState = THUMB_STATE_WRONG_EXIF;

				// Get rid of the thumbnail if that's what they want - this will force
				// it to be regenerated later.
				BOOL regenMismatchedThumbs = [[[Controller sharedController] preferencesData] 
						regenerateMismatchedThumbs];
				if (YES == regenMismatchedThumbs)
				{
					[thumbnail release];
					thumbnail = nil;
				}
			}
			else
			{
				thumbState = THUMB_STATE_VALID_EXIF;
			}
		}
		else
		{
			if (nil != exifDate)
			{
				thumbState = THUMB_STATE_NOT_IN_EXIF;
			}
		}

		[td setThumbnail: thumbnail];
		[thumbnail release];
		[td setThumbstate: thumbState];

		// Notify interested parties that a thumbnail might be available.
		if (NO == *pExitThread)
		{
			[self performSelectorOnMainThread
				: @selector(thumbAvailable:)
				withObject: td
				waitUntilDone: NO];
		}

		// Hmmm, there isn't an EXIF thumbnail. Add this item to our queue, 
		// we'll generate a thumbnail in the next pass.
		if (nil == [td thumbnail])
		{
			[workListLock lock];
			[threadWorkList addObject: td];
			[workListLock unlockWithCondition: HAS_DATA];
		}
	}
	else
	if (nil == [td thumbnail])
	{
		// Our thumbstate is known and the thumbnail doesn't exist - we'll generate it.

//NSLog(@"generating thumbnail for %@", [td name]);
		NSImage *image = nil;
		if (MEDIA_TYPE_MOVIE == [td mediaType])
		{
			image = [self generateMovieThumbnail: td];
		}
		else
		{
			image = generateThumbnail([td fullPath], maxThumbWidth, maxThumbHeight);
		}

		if (nil != image)
		{
			[td setThumbnail: image];
			[image release];

			if (NO == *pExitThread)
			{
				[self performSelectorOnMainThread
					: @selector(thumbAvailable:)
					withObject: td
					waitUntilDone: NO];
			}

			if (MEDIA_TYPE_MOVIE == [td mediaType])
			{
				[self saveMovieThumbnail: td];
			}
		}
	}
	else
	{
NSLog(@"unexpected state for %@ - state=%d", [td name], thumbState);
	}
}

//------------------------------------------------------------------------------
- (NSString*) getMovieThumbnailFilename:(ThumbData*) td
{
	int length = [[td fullPath] length];
	NSMutableString *thumbFile = [NSMutableString stringWithCapacity: length + 4];
	[thumbFile appendString: [td fullPath]];
	NSString *extension = [[td fullPath] pathExtension];

	[thumbFile deleteCharactersInRange: NSMakeRange(length - [extension length], [extension length])];
	[thumbFile appendString: @"THM"];
	return thumbFile;
}

//------------------------------------------------------------------------------
// If a '.THM' file exists, and it's an image, use that as our thumbnail for the movie
- (NSImage*) getMovieThumbnail:(ThumbData*) td
{
	NSString *thumbFile = [self getMovieThumbnailFilename: td];
	return [[NSImage alloc] initWithContentsOfFile: thumbFile];
}

//------------------------------------------------------------------------------
- (void) saveMovieThumbnail:(ThumbData*) td
{
	NSString *thumbFile = [self getMovieThumbnailFilename: td];

	// Never overwrite an existing file
	if (YES == [[NSFileManager defaultManager] fileExistsAtPath: thumbFile])
	{
		return;
	}

	NSData *imageData = [[td thumbnail] TIFFRepresentation];
	NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:imageData];
	NSDictionary *imageProps = [NSDictionary dictionaryWithObject
		:[NSNumber numberWithFloat:0.9] 
		forKey:NSImageCompressionFactor];
	imageData = [imageRep representationUsingType:NSJPEGFileType properties:imageProps];
	[imageData writeToFile:thumbFile atomically:NO];
}

//------------------------------------------------------------------------------
- (NSImage*) generateMovieThumbnail:(ThumbData*) td
{
	NSImage *image = nil;
	NSString *extension = [[td fullPath] pathExtension];

	// Because some movie decoders are NOT thread safe, and because we asked QuickTime
	// to allow non thread safe usage, we control access to QuickTime ourselves
	[movieLock lock];

	if (YES == [excludedMovieExtensions containsObject: extension])
	{
		// A prior attempt in this folder failed, don't even try
		[mainThreadWorkList addObject: td];
	}
	else
	{
		image = generateThumbnail([td fullPath], maxThumbWidth, maxThumbHeight);
		if (nil == image)
		{
			// If we can't generate a thumbnail for a movie, it may be because the decoder
			// can only be run on the main thread - let's have the main thread give it a try.
			[mainThreadWorkList addObject: td];
			[excludedMovieExtensions addObject: extension];
		}
	}

	[movieLock unlock];

	return image;
}

//------------------------------------------------------------------------------
- (void) kqueueThread:(id) nada
{
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    struct timespec zeroTimeout = { 0, 0 }; // 0 second timeout.
    struct timespec pollTimeout = { 5, 0 }; // 5 second timeout.
	struct kevent ev;

	int queueFD = kqueue();

	int	fd = open([path fileSystemRepresentation], O_EVTONLY, 0);
	int flags = NOTE_RENAME | NOTE_WRITE | NOTE_DELETE | NOTE_ATTRIB | NOTE_EXTEND;

	EV_SET(&ev, fd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, flags, 0, NULL);

	// Specify the path to monitor
	kevent(queueFD, &ev, 1, NULL, 0, &zeroTimeout);

	while (NO == *pExitThread)
	{
		int nEvents = kevent(queueFD, NULL, 0, &ev, 1, &pollTimeout);
		if (nEvents > 0)
		{
			if (EVFILT_VNODE == ev.filter)
			{
				[self refresh];
			}
		}
	}


	[pool release];
}

@end
